package uk.ac.ebi.curtain.storage.sequence.fast;

import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;

import org.apache.commons.lang.NotImplementedException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import uk.ac.ebi.curtain.storage.CompressorStrategy;
import uk.ac.ebi.curtain.storage.GzipFormatCompressor;
import uk.ac.ebi.curtain.storage.sequence.InputRandomAccessStream;
import uk.ac.ebi.curtain.storage.sequence.InputStreamProvider;
import uk.ac.ebi.curtain.storage.sequence.RandomAccessStreamProvider;
import uk.ac.ebi.curtain.storage.sequence.StreamProvider;
import uk.ac.ebi.curtain.utils.CurtainUncheckedException;

public class FastBlockReader implements Closeable {
	private CompressorStrategy strategy;
	private Log log;
	private StreamProvider<InputRandomAccessStream> inProvider;
	private StreamProvider<InputStream> idxProvider;
	private FastSequenceIndex idx;
	private InputRandomAccessStream raf;
	
	public FastBlockReader(File f) {
		this(f,getDefaultStrategy());
	}
	public FastBlockReader(File f,CompressorStrategy strategy) {
		this(new RandomAccessStreamProvider(f), new InputStreamProvider(f.getAbsoluteFile()+".qbi"),strategy);
	}
	public FastBlockReader(StreamProvider<InputRandomAccessStream> in, StreamProvider<InputStream> idx) {
		this(in,idx,getDefaultStrategy());
	}
	private static GzipFormatCompressor getDefaultStrategy() {
		return FastBlockWriter.getDefaultStrategy();
	}
	public FastBlockReader(StreamProvider<InputRandomAccessStream> in, StreamProvider<InputStream> idx,CompressorStrategy strategy) {
		this.inProvider = in;
		this.idxProvider = idx;
		this.strategy = strategy;
	}
	
	public void open() throws IOException {
		_loadIndex();
		raf = getInProvider().openAndGetStream();
	}
	private StreamProvider<InputRandomAccessStream> getInProvider() {
		return inProvider;
	}
	
	@Override
	public void close() throws IOException {
		if(null != raf){
			raf.close();
			raf = null;
		}
	}
	private StreamProvider<InputStream> getIdxProvider() {
		return idxProvider;
	}
	
	private void _loadIndex() throws IOException {
		idx = new FastSequenceIndex(getIdxProvider());
		idx.loadIndex();
		this.getStrategy().setBlockSize(idx.getBlockSize());
	}
	

	private Log getLog() {
		if(log == null){
			log = LogFactory.getLog(this.getClass());
		}
		return log;
	}
	
	private byte[] decompress(long start, long end) throws IOException{
		int len = (int) (end-start);
		byte[] arr = new byte[len];
		this.raf.seek(start);
		int total = 0;
		while(total < len){
			int read = this.raf.read(arr, total, len-total);
			if(read == -1){
				throw new IOException("Not possible to read to position " + end);
			}
			total += read;
		}
		byte[] decompressed = getStrategy().decompressBytes(arr);
		return decompressed;
	}
	private CompressorStrategy getStrategy() {
		return strategy;
	}
	
	public Iterable<String> getAllEntries(){
		return getIterable(new LineIterator(new BlockStreamer(_findAllBlocks())));
	}
	private Iterable<String> getIterable(final LineIterator lineIterator) {
		return new SimpleIterable(lineIterator);
	}
	private static class SimpleIterable implements Iterable<String>{

		private final Iterator<String> iterator;
		public SimpleIterable(final Iterator<String> iterator) {
			this.iterator = iterator;
		}
		@Override
		public Iterator<String> iterator() {
			return iterator;
		}
	}
	
	public Iterable<String> getEntries(Long id){
		return getIterable(new LineIterator(new BlockStreamer(_findBlocks(id) )));
	}
	
	private long[] _findAllBlocks() {
		return this.idx.getAllCompressedBlocks();
	}	
	private long[] _findBlocks(Long id) {
		long[] blocks = this.idx.getCompressedBlocks(id);
		if(null == blocks){
			blocks= new long[0];
		}
		return blocks;
	}

	private class LineIterator  implements Iterator<String>{
		private String curr = null;
		private BlockStreamer bs;
		
		public LineIterator(BlockStreamer bs) {
			this.bs = bs;
		}

		@Override
		public boolean hasNext() {
			curr = bs.readLine();
			return curr != null;
		}

		@Override
		public String next() {
			return curr;
		}

		@Override
		public void remove() {
			throw new NotImplementedException();
		}
		
	}
	
	private class BlockStreamer {
		private long[] blocks;
	    private byte[] mCurrentBlock = null;
	    private int mCurrentOffset = 0;
		private int blockPos;
		
		public BlockStreamer(long[] blocks) {
			this.blocks = blocks;
			blockPos = 1;
		}
		
		/**
	     * @return the number of bytes that can be read (or skipped over) from this input stream without blocking by the
	     * next caller of a method for this input stream. The next caller might be the same thread or another thread.
	     */
	    public int available(){
	        if (mCurrentBlock == null || mCurrentOffset == mCurrentBlock.length) {
	            readBlock();
	        }
	        if (mCurrentBlock == null) {
	            return 0;
	        }
	        return mCurrentBlock.length - mCurrentOffset;
	    }

		
		private void readBlock(){
			if(blockPos < this.blocks.length){
	        	mCurrentBlock = null; // set null first - free up possible memory
				mCurrentBlock = _uncompressNext();
				mCurrentOffset = 0;
			} else {
				mCurrentBlock = null;
			}
		}
		
		private byte[] _uncompressNext() {
			long start = this.blocks[blockPos-1];
			long end = this.blocks[blockPos++];
			try{
				return decompress(start, end);
			} catch (IOException e) {
				throw new CurtainUncheckedException("Problems loading block " +start + " to " + end );
			}
		}
		private ByteArrayOutputStream buf = new ByteArrayOutputStream(8192);
		private final byte eol = '\n';
	    private final byte eolCr = '\r';
	    /**
	     * Reads a whole line. A line is considered to be terminated by either a line feed ('\n'), 
	     * carriage return ('\r') or carriage return followed by a line feed ("\r\n").
	     *
	     * @return  A String containing the contents of the line, excluding the line terminating
	     *          character, or null if the end of the stream has been reached
	     *
	     * @exception  IOException  If an I/O error occurs
	     */
	    public String readLine() {
	    	int available = available();
	        if (available == 0) {
	            return null;
	        }
	        buf.reset();
	    	boolean done = false;
	    	boolean foundCr = false; // \r found flag
	        while (!done) {
	        	int linetmpPos = mCurrentOffset;
	        	int bCnt = 0;
	        	while((available-- > 0)){
	        		final byte c = mCurrentBlock[linetmpPos++];
	        		if(c == eol){ // found \n
	        			done = true;
	        			break;
	        		} else if(foundCr){  // previous char was \r
	        			--linetmpPos; // current char is not \n so put it back
	        			done = true;
	        			break;
	        		} else if(c == eolCr){ // found \r
						foundCr = true;
	        			continue; // no ++bCnt
	        		}
					++bCnt;
	        	}
	        	if(mCurrentOffset < linetmpPos){
					buf.write(mCurrentBlock, mCurrentOffset, bCnt);
		        	mCurrentOffset = linetmpPos;
	        	}
	        	available = available();    
	        	if(available == 0){
	        		// EOF
	        		done = true;
	        	}
	        }
	    	return buf.toString();
	    }
	}

	public FastSequenceIndex getIndex() {
		return this.idx;
	}
}